/******************************************************************
*
*	CyberLink for Java
*
*	Copyright (C) Satoshi Konno 2002-2004
*
*	File: Device.java
*
*	Revision:
*
*	11/28/02
*		- first revision.
*	02/26/03
*		- URLBase is updated automatically.
* 		- Description of a root device is returned from the XML node tree.
*	05/13/03
*		- URLBase is updated when the request is received.
*		- Changed to create socket threads each local interfaces.
*		  (HTTP, SSDPSearch)
*	06/17/03
*		- Added notify all state variables when a new subscription is received.
*	06/18/03
*		- Fixed a announce bug when the bind address is null on J2SE v 1.4.1_02 and Redhat 9.
*	09/02/03
*		- Giordano Sassaroli <sassarol@cefriel.it>
*		- Problem : bad request response sent even with successful subscriptions
*		- Error : a return statement is missing in the httpRequestReceived method
*	10/21/03
*		- Updated a udn field by a original uuid.
*	10/22/03
*		- Added setActionListener().
*		- Added setQueryListener().
*	12/12/03
*		- Added a static() to initialize UPnP class.
*	12/25/03
*		- Added advertiser functions.
*	01/05/04
*		- Added isExpired().
*	03/23/04
*		- Oliver Newell <newell@media-rush.com>
*		- Changed to update the UDN only when the field is null.
*	04/21/04
*		- Added isDeviceType().
*	06/18/04
*		- Added setNMPRMode() and isNMPRMode().
*		- Changed getDescriptionData() to update only when the NMPR mode is false.
*	06/21/04
*		- Changed start() to send a bye-bye before the announce.
*		- Changed annouce(), byebye() and deviceSearchReceived() to send the SSDP
*		  messsage four times when the NMPR and the Wireless mode are true.
*	07/02/04
*		- Fixed announce() and byebye() to send the upnp::rootdevice message despite embedded devices.
*		- Fixed getRootNode() to return the root node when the device is embedded.
*	07/24/04
*		- Thanks for Stefano Lenzi <kismet-sl@users.sourceforge.net>
*		- Added getParentDevice().
*	
******************************************************************/

package org.cybergarage.upnp;

import java.net.*;
import java.io.*;
import java.util.*;

import java.util.logging.Logger;

import org.cybergarage.net.*;
import org.cybergarage.http.*;
import org.cybergarage.util.*;
import org.cybergarage.xml.*;
import org.cybergarage.soap.*;

import org.cybergarage.upnp.ssdp.*;
import org.cybergarage.upnp.device.*;
import org.cybergarage.upnp.control.*;
import org.cybergarage.upnp.event.*;
import org.cybergarage.upnp.xml.*;

public class Device implements org.cybergarage.http.HTTPRequestListener, SearchListener
{
  private static Logger logger = Logger.getLogger("org.cybergarage.upnp");

	////////////////////////////////////////////////
	//	Constants
	////////////////////////////////////////////////
	
	public final static String ELEM_NAME = "device";
	public final static String UPNP_ROOTDEVICE = "upnp:rootdevice";

	public final static int DEFAULT_STARTUP_WAIT_TIME = 1000;
	public final static int DEFAULT_DISCOVERY_WAIT_TIME = 300;
	public final static int DEFAULT_LEASE_TIME = 30 * 60;

	public final static int HTTP_DEFAULT_PORT = 4004;

	public final static String DEFAULT_DESCRIPTION_URI = "/description.xml";
	
	////////////////////////////////////////////////
	//	Member
	////////////////////////////////////////////////

	private Node rootNode;
	private Node deviceNode;

	public Node getRootNode()
	{
		if (rootNode != null)
			return rootNode;
		if (deviceNode == null)
			return null;
		return deviceNode.getRootNode();
	}

	public Node getDeviceNode()
	{
		return deviceNode;
	}

	public void setRootNode(Node node)
	{
		rootNode = node;
	}

	public void setDeviceNode(Node node)
	{
		deviceNode = node;
	}
				
	////////////////////////////////////////////////
	//	Initialize
	////////////////////////////////////////////////
	
	static 
	{
		UPnP.initialize();
	}
	
	////////////////////////////////////////////////
	//	Constructor
	////////////////////////////////////////////////

    /** 
     * This form of the constructor is used quite a bit at run-time to
     * 'cast' a rootNode to a Device object. I changed this to not
     * call creatUUID every time, since that is not necessary in the
     * most common usage (moved that logic to the one-time constructor
     * forms)
     *
     * TODO:  Rework UPnP package to be Device-centric, not rootNode
     * centric if possible to reduce (my) confusion
     */
	public Device(Node root, Node device)
	{
		rootNode = root;
		deviceNode = device;
		//setUUID(UPnP.createUUID());
		//setWirelessMode(false);
	}

	public Device()
	{
		this(null, null);
		setUUID(UPnP.createUUID());
		setWirelessMode(true);
	}
	
	public Device(Node device)
	{
		this(null, device);
		setUUID(UPnP.createUUID());
		setWirelessMode(true);
	}

	public Device(File descriptionFile) throws InvalidDescriptionException
	{
		this(null, null);
		setUUID(UPnP.createUUID());
		setWirelessMode(true);
		loadDescription(descriptionFile);
	}

	public Device(String descriptionFileName) throws InvalidDescriptionException
	{
		this(new File(descriptionFileName));
	}

	////////////////////////////////////////////////
	// Mutex
	////////////////////////////////////////////////
	
	private Mutex mutex = new Mutex();
	
	public void lock()
	{
		mutex.lock();
	}
	
	public void unlock()
	{
		mutex.unlock();
	}
	
	////////////////////////////////////////////////
	//	NMPR
	////////////////////////////////////////////////
	
	public void setNMPRMode(boolean flag)
	{
		Node devNode = getDeviceNode();
		if (devNode == null)
			return;
		if (flag == true) {
			devNode.setNode(UPnP.INMPR03, UPnP.INMPR03_VERSION);
			devNode.removeNode(Device.URLBASE_NAME);
		}
		else {
			devNode.removeNode(UPnP.INMPR03);
		}
	}

	public boolean isNMPRMode()
	{
		Node devNode = getDeviceNode();
		if (devNode == null)
			return false;
		return (devNode.getNode(UPnP.INMPR03) != null) ? true : false;
	}
	
	////////////////////////////////////////////////
	//	Wireless
	////////////////////////////////////////////////
	
	private boolean wirelessMode;
	
	public void setWirelessMode(boolean flag)
	{
		wirelessMode = flag;
	}

	public boolean isWirelessMode()
	{
		return wirelessMode;
	}

	public int getSSDPAnnounceCount()
	{
		if (isNMPRMode() == true && isWirelessMode() == true)
			return UPnP.INMPR03_DISCOVERY_OVER_WIRELESS_COUNT;
		return 1;
	}

	////////////////////////////////////////////////
	//	Device UUID
	////////////////////////////////////////////////

	private String devUUID;
	
	public void setUUID(String uuid)
	{
		devUUID = uuid;
	}
	
	public String getUUID() 
	{
		return devUUID;
	}
	
	public void updateUDN()
	{
		setUDN("uuid:" + getUUID());	
	}
	
	////////////////////////////////////////////////
	//	Root Device
	////////////////////////////////////////////////
	
	public Device getRootDevice()
	{
		Node rootNode = getRootNode();
		if (rootNode == null)
			return null;
		Node devNode = rootNode.getNode(Device.ELEM_NAME);
		if (devNode == null)
			return null;
		return new Device(rootNode, devNode);
	}

	////////////////////////////////////////////////
	//	Parent Device
	////////////////////////////////////////////////
	
	// Thanks for Stefano Lenzi (07/24/04)

	public Device getParentDevice()
	{ 
		if(isRootDevice())
			return null;
		Node devNode = getDeviceNode();
		//<device><deviceList><device>
		devNode = devNode.getParentNode().getParentNode().getNode(Device.ELEM_NAME);
		return new Device(devNode);
	}

	////////////////////////////////////////////////
	//	UserData
	////////////////////////////////////////////////

	private DeviceData getDeviceData()
	{
		logger.fine("get device data");
		Node node = getDeviceNode();
		DeviceData userData = (DeviceData)node.getUserData();
		if (userData == null) {
			userData = new DeviceData();
			node.setUserData(userData);
			userData.setNode(node);
		}
		logger.fine("end get device data");
		return userData;
	}
	
	////////////////////////////////////////////////
	//	Description
	////////////////////////////////////////////////

	private void setDescriptionFile(File file)
	{
		getDeviceData().setDescriptionFile(file);
	}

	public File getDescriptionFile()
	{
		return getDeviceData().getDescriptionFile();
	}

	private void setDescriptionURI(String uri)
	{
		getDeviceData().setDescriptionURI(uri);
	}

	private String getDescriptionURI()
	{
		return getDeviceData().getDescriptionURI();
	}

	private boolean isDescriptionURI(String uri)
	{
		String descriptionURI = getDescriptionURI();
		if (uri == null || descriptionURI == null)
			return false;
		return descriptionURI.equals(uri);
	}

  /**
   * Return the path portion of the description file.
   *
   *  Example:  For the file '/home/machine/upnp/device/description.xml'
   *  the path is '/home/machine/upnp/device'  (no trailing slash)
   */ 
	public String getDescriptionFilePath()
	{
		File descriptionFile = getDescriptionFile();
		if (descriptionFile == null)
			return "";
		return descriptionFile.getAbsoluteFile().getParent();
	}

	public boolean loadDescription(File file) throws InvalidDescriptionException
	{
    logger.fine("Loading description file: " + file.toString() );

		try {
			Parser parser = UPnP.getXMLParser();
			rootNode = parser.parse(file);
			if (rootNode == null)
				throw new InvalidDescriptionException(Description.NOROOT_EXCEPTION, file);
			deviceNode = rootNode.getNode(Device.ELEM_NAME);
			if (deviceNode == null)
				throw new InvalidDescriptionException(Description.NOROOTDEVICE_EXCEPTION, file);
		}
		catch (ParserException e) {
			throw new InvalidDescriptionException(e);
		}
		
		setDescriptionFile(file);
		setDescriptionURI(DEFAULT_DESCRIPTION_URI);
		setLeaseTime(DEFAULT_LEASE_TIME);
		setHTTPPort(HTTP_DEFAULT_PORT);

		// Thanks for Oliver Newell (03/23/04)
		if (hasUDN() == false)
			updateUDN();
				    
    		logger.fine("Loaded description file - original URLBase: " + 
                
		getURLBase() );



		return true;
	}

  /**
   *  TODO: URLBase logic is still pretty ugly - clean it up by figuring
   *  it out up front when the device is discovered. Right now Clink deals
   *  with it in lots of separate places.
   */
  public String getResolvedURLBase()
  {
    String urlBaseStr = getURLBase();
  
    if (urlBaseStr == null || urlBaseStr.length() <= 0)
    {
      String location = getLocation();
      String locationHost = HTTP.getHost(location);
      int locationPort = HTTP.getPort(location);
      urlBaseStr = HTTP.getRequestHostURL(locationHost, locationPort);
    }
    return urlBaseStr;
  }

  public String getHost()
  {
    String location = getLocation();
    return HTTP.getHost(location);
  }
  public int getPort()
  {
    String location = getLocation();
    return HTTP.getPort(location);
  }
  

	////////////////////////////////////////////////
	//	isDeviceNode
	////////////////////////////////////////////////

	public static boolean isDeviceNode(Node node)
	{
		return Device.ELEM_NAME.equals(node.getName());
	}
	
	////////////////////////////////////////////////
	//	Root Device
	////////////////////////////////////////////////

	public boolean isRootDevice()
	{
		return (getRootNode() != null) ? true : false;
	}
	
	////////////////////////////////////////////////
	//	Root Device
	////////////////////////////////////////////////

	public void setSSDPPacket(SSDPPacket packet)
	{
		getDeviceData().setSSDPPacket(packet);
	}

	public SSDPPacket getSSDPPacket()
	{
		if (isRootDevice() == false)
			return null;
		return getDeviceData().getSSDPPacket();
	}
	
	////////////////////////////////////////////////
	//	Location 
	////////////////////////////////////////////////

	public void setLocation(String value)
	{
		getDeviceData().setLocation(value);
	}

	public String getLocation()
	{
		SSDPPacket packet = getSSDPPacket();
		if (packet != null)
			return packet.getLocation();
		return getDeviceData().getLocation();
	}

	////////////////////////////////////////////////
	//	LeaseTime 
	////////////////////////////////////////////////

	public void setLeaseTime(int value)
	{
		getDeviceData().setLeaseTime(value);
		Advertiser adv = getAdvertiser();
		if (adv != null) {
			announce();
			adv.restart();
		}
	}

	public int getLeaseTime()
	{
		SSDPPacket packet = getSSDPPacket();
		if (packet != null)
			return packet.getLeaseTime();	
		return getDeviceData().getLeaseTime();
	}

	////////////////////////////////////////////////
	//	TimeStamp 
	////////////////////////////////////////////////

	public long getTimeStamp()
	{
		SSDPPacket packet = getSSDPPacket();
		if (packet != null)
			return packet.getTimeStamp();		
		return 0;
	}

	public long getElapsedTime()
	{
		return (System.currentTimeMillis() - getTimeStamp()) / 1000;
	}

	public boolean isExpired()
	{
		long elapsedTime = getElapsedTime();
		long leaseTime = getLeaseTime() + UPnP.DEFAULT_EXPIRED_DEVICE_EXTRA_TIME;
		if (elapsedTime > leaseTime) {
			logger.warning("Device timeout. Elapsed time: " + elapsedTime + 
                     " LeaseTime(+60sec padding): " + leaseTime );
			return true;
		}
    
		return false;
	}
	
	////////////////////////////////////////////////
	//	URL Base
	////////////////////////////////////////////////

	private final static String URLBASE_NAME = "URLBase";
	
	private void setURLBase(String value)
	{
		if (isRootDevice() == true) {
			Node node = getRootNode().getNode(URLBASE_NAME);
			if (node != null) {
				node.setValue(value);
				return;
			}
			node = new Node(URLBASE_NAME);
			node.setValue(value);
			int index = 1;
			if (getRootNode().hasNodes() == false)
				index = 1;
			getRootNode().insertNode(node, index);
		}
	}

	private void updateURLBase(String host)
	{
		String urlBase = HostInterface.getHostURL(host, getHTTPPort(), "");
		setURLBase(urlBase);
	}
  
	public String getURLBase()
	{
		if (isRootDevice() == true)
			return getRootNode().getNodeValue(URLBASE_NAME);
		return "";
	}

	////////////////////////////////////////////////
	//	deviceType
	////////////////////////////////////////////////

	private final static String DEVICE_TYPE = "deviceType";
	
	public void setDeviceType(String value)
	{
		getDeviceNode().setNode(DEVICE_TYPE, value);
	}

	public String getDeviceType()
	{
		return getDeviceNode().getNodeValue(DEVICE_TYPE);
	}

	public boolean isDeviceType(String value)
	{
		if (value == null)
			return false;
		return value.equals(getDeviceType());
	}

	////////////////////////////////////////////////
	//	friendlyName
	////////////////////////////////////////////////

	private final static String FRIENDLY_NAME = "friendlyName";
	
	public void setFriendlyName(String value)
	{
		getDeviceNode().setNode(FRIENDLY_NAME, value);
	}

	public String getFriendlyName()
	{
		return getDeviceNode().getNodeValue(FRIENDLY_NAME);
	}

	////////////////////////////////////////////////
	//	manufacture
	////////////////////////////////////////////////

	private final static String MANUFACTURER = "manufacturer";
	
	public void setManufacturer(String value)
	{
		getDeviceNode().setNode(MANUFACTURER, value);
	}

	public String getManufacturer()
	{
		return getDeviceNode().getNodeValue(MANUFACTURER);
	}

	////////////////////////////////////////////////
	//	manufacturerURL
	////////////////////////////////////////////////

	private final static String MANUFACTURER_URL = "manufacturerURL";
	
	public void setManufacturerURL(String value)
	{
		getDeviceNode().setNode(MANUFACTURER_URL, value);
	}

	public String getManufacturerURL()
	{
		return getDeviceNode().getNodeValue(MANUFACTURER_URL);
	}

	////////////////////////////////////////////////
	//	modelDescription
	////////////////////////////////////////////////

	private final static String MODEL_DESCRIPTION = "modelDescription";
	
	public void setModelDescription(String value)
	{
		getDeviceNode().setNode(MODEL_DESCRIPTION, value);
	}

	public String getModelDescription()
	{
		return getDeviceNode().getNodeValue(MODEL_DESCRIPTION);
	}

	////////////////////////////////////////////////
	//	modelName
	////////////////////////////////////////////////

	private final static String MODEL_NAME = "modelName";
	
	public void setModelName(String value)
	{
		getDeviceNode().setNode(MODEL_NAME, value);
	}

	public String getModelName()
	{
		return getDeviceNode().getNodeValue(MODEL_NAME);
	}

	////////////////////////////////////////////////
	//	modelNumber
	////////////////////////////////////////////////

	private final static String MODEL_NUMBER = "modelNumber";
	
	public void setModelNumber(String value)
	{
		getDeviceNode().setNode(MODEL_NUMBER, value);
	}

	public String getModelNumber()
	{
		return getDeviceNode().getNodeValue(MODEL_NUMBER);
	}

	////////////////////////////////////////////////
	//	modelURL
	////////////////////////////////////////////////

	private final static String MODEL_URL = "modelURL";
	
	public void setModelURL(String value)
	{
		getDeviceNode().setNode(MODEL_URL, value);
	}

	public String getModelURL()
	{
		return getDeviceNode().getNodeValue(MODEL_URL);
	}

	////////////////////////////////////////////////
	//	serialNumber
	////////////////////////////////////////////////

	private final static String SERIAL_NUMBER = "serialNumber";
	
	public void setSerialNumber(String value)
	{
		getDeviceNode().setNode(SERIAL_NUMBER, value);
	}

	public String getSerialNumber()
	{
		return getDeviceNode().getNodeValue(SERIAL_NUMBER);
	}

	////////////////////////////////////////////////
	//	UDN
	////////////////////////////////////////////////

	private final static String UDN = "UDN";
	
	public void setUDN(String value)
	{
		getDeviceNode().setNode(UDN, value);
	}

	public String getUDN()
	{
		return getDeviceNode().getNodeValue(UDN);
	}

	public boolean hasUDN()
	{
		String udn = getUDN();
		if (udn == null || udn.length() <= 0)
			return false;
		return true;
	}
	
	////////////////////////////////////////////////
	//	UPC
	////////////////////////////////////////////////

	private final static String UPC = "UPC";
	
	public void setUPC(String value)
	{
		getDeviceNode().setNode(UPC, value);
	}

	public String getUPC()
	{
		return getDeviceNode().getNodeValue(UPC);
	}

	////////////////////////////////////////////////
	//	presentationURL
	////////////////////////////////////////////////

	private final static String presentationURL = "presentationURL";
	
	public void setPresentationURL(String value)
	{
		getDeviceNode().setNode(presentationURL, value);
	}

	public String getPresentationURL()
	{
		return getDeviceNode().getNodeValue(presentationURL);
	}

	////////////////////////////////////////////////
	//	deviceList
	////////////////////////////////////////////////

	public DeviceList getDeviceList()
	{
		logger.fine("getDeviceList");
		DeviceList devList = new DeviceList();
		Node devListNode = getDeviceNode().getNode(DeviceList.ELEM_NAME);
		if (devListNode == null)
			return devList;
		int nNode = devListNode.getNNodes();
		for (int n=0; n<nNode; n++) {
			Node node = devListNode.getNode(n);
			if (Device.isDeviceNode(node) == false)
				continue;
			Device dev = new Device(node);
			devList.add(dev);
		} 
		return devList;
	}

	public boolean isDevice(String name)
	{
		if (name == null)
			return false;
		if (name.endsWith(getUDN()) == true)
			return true;
		if (name.equals(getFriendlyName()) == true)
			return true;
		if (name.endsWith(getDeviceType()) == true)
			return true;
		return false;
	}
	
	public Device getDevice(String name)
	{
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			if (dev.isDevice(name) == true)
				return dev;
			Device cdev = dev.getDevice(name);
			if (cdev != null)
				return cdev;
		}
		return null;
	}
	
	////////////////////////////////////////////////
	//	serviceList
	////////////////////////////////////////////////

	public ServiceList getServiceList()
	{
		logger.fine("getServiceList");
		ServiceList serviceList = new ServiceList();
		Node serviceListNode = getDeviceNode().getNode(ServiceList.ELEM_NAME);
		if (serviceListNode == null)
			return serviceList;
		int nNode = serviceListNode.getNNodes();
		for (int n=0; n<nNode; n++) {
			Node node = serviceListNode.getNode(n);
			if (Service.isServiceNode(node) == false)
				continue;
			Service service = new Service(node);
			serviceList.add(service);
		} 
		return serviceList;
	}

	public Service getService(String name)
	{
		logger.fine(String.format("getService: %s", name));
		ServiceList serviceList = getServiceList();
		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			if (service.isService(name) == true)
				return service;
		}
		
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			Service service = dev.getService(name);
			if (service != null)
				return service;
		}
		
		return null;
	}

	private Service getServiceByControlURL(String searchUrl)
	{
		logger.fine("getServiceControlURL");
		ServiceList serviceList = getServiceList();
		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			if (service.isControlURL(searchUrl) == true)
				return service;
		}
		
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			Service service = dev.getServiceByControlURL(searchUrl);
			if (service != null)
				return service;
		}
		
		return null;
	}

	private Service getServiceByEventSubURL(String searchUrl)
	{
		ServiceList serviceList = getServiceList();
		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			if (service.isEventSubURL(searchUrl) == true)
				return service;
		}
		
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			Service service = dev.getServiceByEventSubURL(searchUrl);
			if (service != null)
				return service;
		}
		
		return null;
	}

	public Service getSubscriberService(String uuid)
	{
    logger.finer( "getSubscriberService - deviceName = " + getFriendlyName() );

		ServiceList serviceList = getServiceList();
		int serviceCnt = serviceList.size();
    logger.finer( "getSubscriberService - uuid = " + uuid + " nDeviceServices = " + serviceCnt );
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			String sid = service.getSID();
      logger.finer( "Service # " + n + " SID = " + sid );
			if (uuid.equals(sid) == true)
      {
        logger.finer( "getSubscriberService - found match " );
				return service;
      }
		}
		
    logger.finer( "no matching service found - checking embedded devices " );

		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			Service service = dev.getSubscriberService(uuid);
			if (service != null)
				return service;
		}
		
		return null;
	}

	////////////////////////////////////////////////
	//	StateVariable
	////////////////////////////////////////////////

	public StateVariable getStateVariable(String name)
	{
		ServiceList serviceList = getServiceList();
		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			StateVariable stateVar = service.getStateVariable(name);
			if (stateVar != null)
				return stateVar;
		}
		
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			StateVariable stateVar = dev.getStateVariable(name);
			if (stateVar != null)
				return stateVar;
		}
		
		return null;
	}

	////////////////////////////////////////////////
	//	Action
	////////////////////////////////////////////////

	public Action getAction(String name)
	{
		ServiceList serviceList = getServiceList();
		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			ActionList actionList = service.getActionList();
			int actionCnt = actionList.size();
			for (int i=0; i<actionCnt; i++) {
				Action action = (Action)actionList.getAction(i);
				String actionName = action.getName();
				if (actionName == null)
					continue;
				if (actionName.equals(name) == true)
					return action;
			}
		}
		
		DeviceList devList = getDeviceList();
		int devCnt = devList.size();
		for (int n=0; n<devCnt; n++) {
			Device dev = devList.getDevice(n);
			Action action = dev.getAction(name);
			if (action != null)
				return action;
		}
		
		return null;
	}

	////////////////////////////////////////////////
	//	iconList
	////////////////////////////////////////////////

	public IconList getIconList()
	{
		IconList iconList = new IconList();
		Node iconListNode = getDeviceNode().getNode(IconList.ELEM_NAME);
		if (iconListNode == null)
			return iconList;
		int nNode = iconListNode.getNNodes();
		for (int n=0; n<nNode; n++) {
			Node node = iconListNode.getNode(n);
			if (Icon.isIconNode(node) == false)
				continue;
			Icon icon = new Icon(node);
			iconList.add(icon);
		} 
		return iconList;
	}
	
	public Icon getIcon(int n)
	{
		IconList iconList = getIconList();
		if (n < 0 && (iconList.size()-1) < n)
			return null;
		return iconList.getIcon(n);
	}

	////////////////////////////////////////////////
	//	Notify
	////////////////////////////////////////////////

	public String getLocationURL(String host)
	{
		return HostInterface.getHostURL(host, getHTTPPort(), getDescriptionURI());
	}

	private String getNotifyDeviceNT()
	{
		if (isRootDevice() == false)
			return getUDN();			
		return UPNP_ROOTDEVICE;
	}

	private String getNotifyDeviceUSN()
	{
		if (isRootDevice() == false)
			return getUDN();			
		return getUDN() + "::" + UPNP_ROOTDEVICE;
	}

	private String getNotifyDeviceTypeNT()
	{
		return getDeviceType();
	}

	private String getNotifyDeviceTypeUSN()
	{
		return getUDN() + "::" + getDeviceType();
	}
	
	public final static void notifyWait()
	{
		TimerUtil.waitRandom(DEFAULT_DISCOVERY_WAIT_TIME);
	}
		
	public void announce(String bindAddr)
	{
		String devLocation = getLocationURL(bindAddr);
		
		SSDPNotifySocket ssdpSock = new SSDPNotifySocket(bindAddr);

		SSDPNotifyRequest ssdpReq = new SSDPNotifyRequest();
		ssdpReq.setServer(UPnP.getServerName());
		ssdpReq.setLeaseTime(getLeaseTime());
		ssdpReq.setLocation(devLocation);
		ssdpReq.setNTS(NTS.ALIVE);
		
		// uuid:device-UUID(::upnp:rootdevice)* 
		if (isRootDevice() == true) {
			String devNT = getNotifyDeviceNT();			
			String devUSN = getNotifyDeviceUSN();
			ssdpReq.setNT(devNT);
			ssdpReq.setUSN(devUSN);
			ssdpSock.post(ssdpReq);
		}
		
		// uuid:device-UUID::urn:schemas-upnp-org:device:deviceType:v 
		String devNT = getNotifyDeviceTypeNT();			
		String devUSN = getNotifyDeviceTypeUSN();
		ssdpReq.setNT(devNT);
		ssdpReq.setUSN(devUSN);
		ssdpSock.post(ssdpReq);
		
		ServiceList serviceList = getServiceList();
		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			service.announce(bindAddr);
		}

		DeviceList childDeviceList = getDeviceList();
		int childDeviceCnt = childDeviceList.size();
		for (int n=0; n<childDeviceCnt; n++) {
			Device childDevice = childDeviceList.getDevice(n);
			childDevice.announce(bindAddr);
		}
	}

	public void announce()
	{
		logger.fine("Announce");
		notifyWait();
		
		int nHostAddrs = HostInterface.getNHostAddresses();
		for (int n=0; n<nHostAddrs; n++) {
			String bindAddr = HostInterface.getHostAddress(n);
			if (bindAddr == null || bindAddr.length() <= 0)
				continue;
			int ssdpCount = getSSDPAnnounceCount();
			for (int i=0; i<ssdpCount; i++)
				announce(bindAddr);
		}
	}
	
	public void byebye(String bindAddr)
	{
		SSDPNotifySocket ssdpSock = new SSDPNotifySocket(bindAddr);
		
		SSDPNotifyRequest ssdpReq = new SSDPNotifyRequest();
		ssdpReq.setNTS(NTS.BYEBYE);

		// uuid:device-UUID(::upnp:rootdevice)* 
		if (isRootDevice() == true) {
			String devNT = getNotifyDeviceNT();			
			String devUSN = getNotifyDeviceUSN();
			ssdpReq.setNT(devNT);
			ssdpReq.setUSN(devUSN);
			ssdpSock.post(ssdpReq);
		}
		
		// uuid:device-UUID::urn:schemas-upnp-org:device:deviceType:v 
		String devNT = getNotifyDeviceTypeNT();			
		String devUSN = getNotifyDeviceTypeUSN();
		ssdpReq.setNT(devNT);
		ssdpReq.setUSN(devUSN);
		ssdpSock.post(ssdpReq);

		ServiceList serviceList = getServiceList();
		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			service.byebye(bindAddr);
		}

		DeviceList childDeviceList = getDeviceList();
		int childDeviceCnt = childDeviceList.size();
		for (int n=0; n<childDeviceCnt; n++) {
			Device childDevice = childDeviceList.getDevice(n);
			childDevice.byebye(bindAddr);
		}
	}

	public void byebye()
	{
		int nHostAddrs = HostInterface.getNHostAddresses();
    logger.info("nHostAddrs = " + nHostAddrs );
    
		for (int n=0; n<nHostAddrs; n++) {
			String bindAddr = HostInterface.getHostAddress(n);

      logger.info("HostAddrs " + n + " = " + bindAddr );

			if (bindAddr == null || bindAddr.length() <= 0)
				continue;
			int ssdpCount = getSSDPAnnounceCount();
			for (int i=0; i<ssdpCount; i++)
				byebye(bindAddr);
		}
	}

	////////////////////////////////////////////////
	//	Search
	////////////////////////////////////////////////

  /**
   * Post a response to a search request. 
   *
   * @param  ssdpPacket  
   *
   */
	public boolean postSearchResponse(SSDPPacket ssdpPacket, String st,
                                    String usn)
	{
		String localAddr = ssdpPacket.getLocalAddress();
		Device rootDev = getRootDevice();
		String rootDevLocation = rootDev.getLocationURL(localAddr);
		
		SSDPSearchResponse ssdpRes = new SSDPSearchResponse();
		ssdpRes.setLeaseTime(getLeaseTime());
		ssdpRes.setDate(Calendar.getInstance());
		ssdpRes.setST(st);
		ssdpRes.setUSN(usn);
		ssdpRes.setLocation(rootDevLocation);

		int mx = ssdpPacket.getMX();
		TimerUtil.waitRandom(mx * 1000);
		
		String remoteAddr = ssdpPacket.getRemoteAddress();
		int remotePort = ssdpPacket.getRemotePort();
		SSDPSearchResponseSocket ssdpResSock = new SSDPSearchResponseSocket();
		if (Debug.isOn() == true)
			ssdpRes.print();
		int ssdpCount = getSSDPAnnounceCount();
		for (int i=0; i<ssdpCount; i++)
		    {
			ssdpResSock.post(remoteAddr, remotePort, ssdpRes);
		    }

		return true;
	}
	
  /**
   * Respond to a search request. The type of the response depends on the
   * request's search target field
   *
   * @param  ssdpPacket
   *
   *         Incoming SSDP search request packet. There are several types
   *         of search requests. Here's one example:
   *
   *         M-SEARCH * HTTP/1.1
   *         ST: ssdp:all
   *         MX: 3
   *         MAN: "ssdp:discover"
   *         HOST: 239.255.255.250:1900
   *
   *         The 'ST' (search target) is set depending on the search type:
   *
   *         1.  Search for all Root device search request.
   *
   *           ST: ssdp:all
   *
   *         2.  Root device search request
   *
   *           ST: upnp:rootdevice
   *     
   *         3.  Device specific search request (not limited to these examples)
   *
   *           ST: urn:schemas-upnp-org:device:MediaServer:1
   *      
   *          or
   *
   *           ST: urn:schemas-upnp-org:device:MediaServer:1
   *
   *
   */
	public void deviceSearchResponse(SSDPPacket ssdpPacket)
	{
		String ssdpST = ssdpPacket.getST();

		//logger.info("deviceSearchResponse - Entered");

		if (ssdpST == null)
			return;

		boolean isRootDevice = isRootDevice();
		
		String devUSN = getUDN();
			
    //  ST = ssdp:all ?
		if (ST.isAllDevice(ssdpST) == true)
    {
      if (isRootDevice == true)
        devUSN += "::" + USN.ROOTDEVICE;

			String devNT = getNotifyDeviceNT();			
			int repeatCnt = (isRootDevice == true) ? 3 : 2;
			for (int n=0; n<repeatCnt; n++)
				postSearchResponse(ssdpPacket, ssdpST, devUSN );
		}
    //  ST = upnp:rootdevice ?
		else if (ST.isRootDevice(ssdpST) == true) {
			if (isRootDevice == true)
				postSearchResponse(ssdpPacket, ssdpST,
                           devUSN + "::" + USN.ROOTDEVICE);
		}
		else if (ST.isUUIDDevice(ssdpST) == true) {
			String devUDN = getUDN();
			if (ssdpST.equals(devUDN) == true)
				postSearchResponse(ssdpPacket, ssdpST, devUSN);
		}
    //  ST = urn:schemas-upnp-org:device:MediaServer:1  (specific example)
		else if (ST.isURNDevice(ssdpST) == true) {
			String devType= getDeviceType();
			if (ssdpST.equals(devType) == true)
				postSearchResponse(ssdpPacket, ssdpST,
                           devUSN + "::" + devType );
		}
		
		ServiceList serviceList = getServiceList();
		int serviceCnt = serviceList.size();
		for (int n=0; n<serviceCnt; n++) {
			Service service = serviceList.getService(n);
			service.serviceSearchResponse(ssdpPacket);
		}
		
		DeviceList childDeviceList = getDeviceList();
		int childDeviceCnt = childDeviceList.size();
		for (int n=0; n<childDeviceCnt; n++) {
			Device childDevice = childDeviceList.getDevice(n);
			childDevice.deviceSearchResponse(ssdpPacket);
		}
	}
	
	public void deviceSearchReceived(SSDPPacket ssdpPacket)
	{
		deviceSearchResponse(ssdpPacket);
	}
	
	////////////////////////////////////////////////
	//	HTTP Server	
	////////////////////////////////////////////////

	public void setHTTPPort(int port)
	{
		getDeviceData().setHTTPPort(port);
	}
	
	public int getHTTPPort()
	{
		return getDeviceData().getHTTPPort();
	}

	public void httpRequestReceived(HTTPRequest httpReq)
	{
		if (Debug.isOn() == true)
			httpReq.print();
	
		if (httpReq.isGetRequest() == true)
    {
      // Get requests are received to retrieve Description/Service XML
			httpGetRequestReceived(httpReq);
			return;
		}
		if (httpReq.isPostRequest() == true)
    {
      // SOAP actions are Post requests
			httpPostRequestReceived(httpReq);
			return;
		}

		if( httpReq.isSubscribeRequest() || httpReq.isUnsubscribeRequest() )
    {
      logger.fine("Got subscribe req:\n" + httpReq.toString());
			SubscriptionRequest subReq = new SubscriptionRequest( httpReq );
			deviceEventSubscriptionRecieved( subReq );
			return;
		}

		httpReq.returnBadRequest();
	}

	private synchronized byte[] getDescriptionData(String host)
	{
		if (isNMPRMode() == false)
			updateURLBase(host);

		Node rootNode = getRootNode();
		if (rootNode == null)
			return new byte[0];

    String descriptionXML = "<?xml version = \"1.0\" encoding=\"utf-8\" ?>\n" +
                            rootNode.toString();

		return descriptionXML.getBytes();
	}
	
	private void httpGetRequestReceived( HTTPRequest httpReq )
	{
		String uri = httpReq.getURI();
		Debug.message("httpGetRequestRecieved = " + uri);
		if (uri == null)
    {
			httpReq.returnBadRequest();
			return;
		}
					
		String rootPath = getDescriptionFilePath();
		byte httpContent[] = new byte[0];

		try
    {
      // If request for description XML, generate the XML using the in-memory
      // device description
			if( isDescriptionURI(uri) == true )
      {
				String localAddr = httpReq.getLocalAddress();
				httpContent = getDescriptionData( localAddr );
			}
			else
      {
        //System.out.println("CLink-level httpReq URI = " + uri );

        //
        // Any other request is assumed to be a request for a URL resource
        // relative to the device's description directory. In some cases,
        // it may be desirable for a device to dynamically generate a 
        // resource (Web page). Resources URLs that are not present as 
        // physical files in the directory are passed on to the application
        // so it can generate them dynamically if it wants
        //
				File docFile = new File( rootPath, uri );
				if (docFile.exists() == false)
        {
          httpContent = generateDynamicHttpContent( uri );
          if( httpContent == null ) 
          {
            logger.warning("Request for unrecognized URI ('" + uri + "')" );
            httpReq.returnBadRequest();
            return;
          }
				}
        else
        {
          httpContent = FileUtil.load( docFile );
          //System.out.println("Loaded " + httpContent.length + " bytes from " + uri );
        }
			}
		}
		catch (Exception e)
    {
			httpReq.returnBadRequest();
			return;
		}

    HTTPResponse httpRes = new HTTPResponse();

    if (FileUtil.isXMLFileName(uri) == true)
      httpRes.setContentType(XML.CONTENT_TYPE);
    else if( uri.endsWith(".css") )
      httpRes.setContentType("text/css");

    httpRes.setStatusCode(HTTPStatus.OK);
    httpRes.setContent( httpContent );
    httpReq.post(httpRes);
	}

	private void httpPostRequestReceived(HTTPRequest httpReq)
	{
		if (httpReq.isSOAPAction() == true) {
			//SOAPRequest soapReq = new SOAPRequest(httpReq);
			soapActionRecieved(httpReq);
		}
	}

  /**
   * Generate dynamic HTTP content for the given URI. If the uri is
   * not recognized, return null
   *
   * This routine is meant to be overridden by device applications that
   * provide support for dynamically generated presentation pages.
   *
   * There's probably a more 'standard' way of doing this - revisit (TODO)
   */
  public byte[] generateDynamicHttpContent( String uri )
  {
    return null;
  }


	////////////////////////////////////////////////
	//	SOAP
	////////////////////////////////////////////////

	private void soapBadActionRecieved(HTTPRequest soapReq)
	{
		SOAPResponse soapRes = new SOAPResponse();
		soapRes.setStatusCode(HTTPStatus.BAD_REQUEST);
		soapReq.post(soapRes);
	}

	private void soapActionRecieved(HTTPRequest soapReq)
	{
		String uri = soapReq.getURI();
		Service ctlService = getServiceByControlURL(uri);
		if (ctlService != null)  {
			ActionRequest crlReq = new ActionRequest(soapReq);
			deviceControlRequestRecieved(crlReq, ctlService);
			return;
		}
		soapBadActionRecieved(soapReq);
	}

	////////////////////////////////////////////////
	//	controlAction
	////////////////////////////////////////////////

	private void deviceControlRequestRecieved(ControlRequest ctlReq, Service service)
	{
		if (ctlReq.isQueryControl() == true)
			deviceQueryControlRecieved(new QueryRequest(ctlReq), service);
		else
			deviceActionControlRecieved(new ActionRequest(ctlReq), service);
	}

	private void invalidActionControlRecieved(ControlRequest ctlReq)
	{
		ControlResponse actRes = new ActionResponse();
		actRes.setFaultResponse(UPnPStatus.INVALID_ACTION);
		ctlReq.post(actRes);
	}

	private void deviceActionControlRecieved(ActionRequest ctlReq, Service service)
	{
		if (Debug.isOn() == true)
			ctlReq.print();
			
		String actionName = ctlReq.getActionName();
		Action action = service.getAction(actionName);
		if (action == null) {
			invalidActionControlRecieved(ctlReq);
			return;
		}
		ArgumentList actionArgList = action.getArgumentList();
		ArgumentList reqArgList = ctlReq.getArgumentList();
		actionArgList.set(reqArgList);
		if (action.performActionListener(ctlReq) == false)
			invalidActionControlRecieved(ctlReq);
	}

	private void deviceQueryControlRecieved(QueryRequest ctlReq, Service service)
	{
		if (Debug.isOn() == true)
			ctlReq.print();
		String varName = ctlReq.getVarName();
		if (service.hasStateVariable(varName) == false) {
			invalidActionControlRecieved(ctlReq);
			return;
		}
		StateVariable stateVar = getStateVariable(varName);
		if (stateVar.performQueryListener(ctlReq) == false)
			invalidActionControlRecieved(ctlReq);
	}

	////////////////////////////////////////////////
	//	eventSubscribe
	////////////////////////////////////////////////

	private void upnpBadSubscriptionRecieved(SubscriptionRequest subReq, int code)
	{
		SubscriptionResponse subRes = new SubscriptionResponse();
		subRes.setErrorResponse(code);
		subReq.post(subRes);
	}

	private void deviceEventSubscriptionRecieved(SubscriptionRequest subReq)
	{
		String uri = subReq.getURI();
		Service service = getServiceByEventSubURL(uri);
		if (service == null) {
			subReq.returnBadRequest();
			return;
		}
		if (subReq.hasCallback() == false && subReq.hasSID() == false) {
			upnpBadSubscriptionRecieved(subReq, HTTPStatus.PRECONDITION_FAILED);
			return;
		}

		// UNSUBSCRIBE
		if (subReq.isUnsubscribeRequest() == true) {
			deviceEventUnsubscriptionRecieved(service, subReq);
			return;
		}

		// SUBSCRIBE (NEW)
		if (subReq.hasCallback() == true) {
			deviceEventNewSubscriptionRecieved(service, subReq);
			return;
		}
		
		// SUBSCRIBE (RENEW)
		if (subReq.hasSID() == true) {
			deviceEventRenewSubscriptionRecieved(service, subReq);
			return;
		}
		
		upnpBadSubscriptionRecieved(subReq, HTTPStatus.PRECONDITION_FAILED);
	}

	private void deviceEventNewSubscriptionRecieved(Service service, SubscriptionRequest subReq)
	{
		String callback = subReq.getCallback();
		try {
			URL url = new URL(callback);
		}
		catch (Exception e) {
			upnpBadSubscriptionRecieved(subReq, HTTPStatus.PRECONDITION_FAILED);
			return;
		}

    //
    // Mod to set the subscription timeout based on the device, not the
    // control point. The value specified by the service is used. 
    // By default, the service timeout is 180 sec, 60 sec more than 
    // the recommended NMPR renew period
    //
		//long timeOut = subReq.getTimeout();
		long timeOut = service.getTimeout();

		String sid = Subscription.createSID();
			
		Subscriber sub = new Subscriber();
		sub.setDeliveryURL(callback);
    sub.setTimeOut( timeOut );
		sub.setSID(sid);
		service.addSubscriber(sub);
			
		SubscriptionResponse subRes = new SubscriptionResponse();
		subRes.setStatusCode(HTTPStatus.OK);
		subRes.setSID(sid);
		subRes.setTimeout(timeOut);
		if (Debug.isOn() == true)
			subRes.print();
		subReq.post(subRes);

		service.notifyAllStateVariables();
	}

	private void deviceEventRenewSubscriptionRecieved(Service service, SubscriptionRequest subReq)
	{
		String sid = subReq.getSID();
		Subscriber sub = service.getSubscriber(sid);

		if (sub == null) {
			upnpBadSubscriptionRecieved(subReq, HTTPStatus.PRECONDITION_FAILED);
			return;
		}

    // See comment in above routine
		//long timeOut = subReq.getTimeout();
		long timeOut = service.getTimeout();

		sub.setTimeOut(timeOut);
		sub.renew();
				
		SubscriptionResponse subRes = new SubscriptionResponse();
		subRes.setStatusCode(HTTPStatus.OK);
		subRes.setSID(sid);
		subRes.setTimeout(timeOut);
		subReq.post(subRes);
	}		

	private void deviceEventUnsubscriptionRecieved(Service service, SubscriptionRequest subReq)
	{
		String sid = subReq.getSID();
		Subscriber sub = service.getSubscriber(sid);

		if (sub == null) {
			upnpBadSubscriptionRecieved(subReq, HTTPStatus.PRECONDITION_FAILED);
			return;
		}

		service.removeSubscriber(sub);
						
		SubscriptionResponse subRes = new SubscriptionResponse();
		subRes.setStatusCode(HTTPStatus.OK);
		subReq.post(subRes);
	}		
	
	////////////////////////////////////////////////
	//	Thread	
	////////////////////////////////////////////////

	private HTTPServerList getHTTPServerList() 
	{
		return getDeviceData().getHTTPServerList();
	}

	private SSDPSearchSocketList getSSDPSearchSocketList() 
	{
		return getDeviceData().getSSDPSearchSocketList();
	}

	private void setAdvertiser(Advertiser adv) 
	{
		getDeviceData().setAdvertiser(adv);
	}
	
	private Advertiser getAdvertiser() 
	{
		return getDeviceData().getAdvertiser();
	}

	public boolean start()
	{
		logger.fine("start");
		stop(true);
		
		////////////////////////////////////////
		// HTTP Server
		////////////////////////////////////////
		
		int retryCnt = 0;
		int bindPort = getHTTPPort();

		logger.fine(String.format("Bind Port: %d", bindPort));

    // Instantiate list of servers, one per interface
		HTTPServerList httpServerList = getHTTPServerList();
		while (httpServerList.open(bindPort) == false) {
			retryCnt++;
			if (UPnP.SERVER_RETRY_COUNT < retryCnt)
				return false;
			setHTTPPort(bindPort + 1);
			bindPort = getHTTPPort();
		}
		httpServerList.addRequestListener(this);
		httpServerList.start();

		////////////////////////////////////////
		// SSDP Seach Socket
		////////////////////////////////////////
		
		SSDPSearchSocketList ssdpSearchSockList = getSSDPSearchSocketList();
		if (ssdpSearchSockList.open() == false)
			return false;
		ssdpSearchSockList.addSearchListener(this);
		ssdpSearchSockList.start();

		////////////////////////////////////////
		// Announce
		////////////////////////////////////////
		
		announce();
		
		////////////////////////////////////////
		// Advertiser
		////////////////////////////////////////

		Advertiser adv = new Advertiser(this);
		setAdvertiser(adv);
		adv.start();
	
		return true;
	}

	private boolean stop(boolean doByeBye)
	{
		logger.fine("stop");
		if (doByeBye == true)
			byebye();
		
		HTTPServerList httpServerList = getHTTPServerList();
		httpServerList.stop();
		httpServerList.close();
		httpServerList.clear();
		
		SSDPSearchSocketList ssdpSearchSockList = getSSDPSearchSocketList();
		ssdpSearchSockList.stop();
		ssdpSearchSockList.close();
		ssdpSearchSockList.clear();
		
		Advertiser adv = getAdvertiser();
		if (adv != null) {
			adv.stop();
			setAdvertiser(null);
		}

		return true;
	}
	
	public boolean stop()
	{
		return stop(true);
	}

	////////////////////////////////////////////////
	// Interface Address
	////////////////////////////////////////////////
	
	public String getInterfaceAddress() 
	{
		SSDPPacket ssdpPacket = getSSDPPacket();
		if (ssdpPacket == null)
			return "";
		return ssdpPacket.getLocalAddress();
	}

	////////////////////////////////////////////////
	// Acion/QueryListener
	////////////////////////////////////////////////
	
	public void setActionListener(ActionListener listener)
	{
		ServiceList serviceList = getServiceList();
		int nServices = serviceList.size();
		for (int n=0; n<nServices; n++) {
			Service service = serviceList.getService(n);
			service.setActionListener(listener);
		}
	}

	public void setQueryListener(QueryListener listener)
	{
		ServiceList serviceList = getServiceList();
		int nServices = serviceList.size();
		for (int n=0; n<nServices; n++) {
			Service service = serviceList.getService(n);
			service.setQueryListener(listener);
		}
	}

  /**
   * Set/get device instance perspective. Valid values are UPnP.DEVICE and
   * UPnP.CONTROL_POINT
   */
	public void setInstancePerspective( int value )
	{
		getDeviceData().setInstancePerspective( value );
	}
	public int getInstancePerspective()
	{
		return getDeviceData().getInstancePerspective();
	}


}

